1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.hipaudio.audioclip; 12 import hip.util.path : baseName; 13 import hip.error.handler; 14 import hip.audio_decoding.audio; 15 import hip.hipaudio.audio; 16 import hip.hipaudio.audiosource; 17 public import hip.api.audio.audioclip; 18 19 20 union HipAudioBuffer 21 { 22 import hip.hipaudio.config; 23 static if(HasOpenAL) 24 { 25 import bindbc.openal; 26 ALuint al; 27 } 28 static if(HasOpenSLES) 29 { 30 import opensles.sles; 31 import hip.hipaudio.backend.sles; 32 SLIBuffer* sles; 33 } 34 static if(HasXAudio2) 35 { 36 import directx.xaudio2; 37 XAUDIO2_BUFFER* xaudio; 38 } 39 static if(HasWebAudio) 40 { 41 import hip.hipaudio.backend.webaudio.clip; 42 size_t webaudio; 43 } 44 static if(HasAVAudioEngine) 45 { 46 import hip.hipaudio.backend.avaudio.clip; 47 AVAudioPCMBuffer avaudio; 48 } 49 } 50 51 struct HipAudioBufferWrapper 52 { 53 HipAudioBuffer buffer; 54 bool isAvailable; 55 } 56 57 58 59 /** 60 * Wraps a decoder onto it. Basically an easier interface with some more controls 61 * that would be needed inside specific APIs. 62 * 63 * AudioClip flow basically consists in: 64 * 1. Initialize the audio clip with the current decoder. 65 * 2. Call `.load`, which calls `.decode` 66 * 3. HipAudioSource calls `.setClip`, which should call `clip.getBuffer`, which gets the buffer 67 * wrapped by the current implementation `createBuffer`, and then the buffer is enqueued. 68 */ 69 public abstract class HipAudioClip : IHipAudioClip 70 { 71 IHipAudioDecoder decoder; 72 ///Unused for non streamed. It is the binary loaded from a file which will be decoded 73 ubyte[] dataToDecode; 74 ///Unused for non streamed. Where the user will get its audio decoded. 75 ubyte[] outBuffer; 76 ///Unused for non streamed 77 uint chunkSize; 78 79 80 HipAudioClipHint hint; 81 82 /** 83 * Buffers recycled from HipAudioSource. 84 * 85 * When source notifies that the buffer is free, it is added to 86 * that array. When getBuffer is called, it could send one 87 * of those recycleds. 88 */ 89 private HipAudioBufferWrapper[] buffersToRecycle; 90 private HipAudioBufferWrapper[] buffersCreated; 91 92 size_t totalDecoded = 0; 93 94 HipAudioType type; 95 HipAudioEncoding encoding; 96 bool isStreamed = false; 97 string fileName; 98 99 100 ///Event method called when the stream is updated 101 protected abstract void onUpdateStream(ubyte[] data, uint decodedSize); 102 /** 103 * Always alocates a pointer to the buffer data. So, after getting its content. Send it to the 104 * recyclable buffers 105 */ 106 protected abstract HipAudioBufferWrapper createBuffer(ubyte[] data); 107 protected abstract void destroyBuffer(HipAudioBuffer* buffer); 108 109 /** The buffer is actually any kind of external API buffer, it is the buffer contained in 110 * HipAudioBufferWrapper. 111 * 112 * OpenAL: `int` containing the buffer ID 113 * OpenSL ES: `SLIBuffer` 114 * XAudio2: To be thought? 115 */ 116 public abstract void setBufferData(HipAudioBuffer* buffer, ubyte[] data, uint size); 117 118 final immutable(HipAudioClipHint)* getHint(){return cast(immutable)&hint;} 119 120 this(IHipAudioDecoder decoder, HipAudioClipHint hint){this.decoder = decoder; this.hint = hint;} 121 this(IHipAudioDecoder decoder, HipAudioClipHint hint, uint chunkSize) 122 in(chunkSize > 0, "Chunk must be greater than 0") 123 { 124 this(decoder, hint); 125 this.chunkSize = chunkSize; 126 outBuffer = new ubyte[chunkSize]; 127 ErrorHandler.assertExit(outBuffer != null, "Out of memory"); 128 } 129 /** 130 * Should implement the specific loading here 131 */ 132 public bool loadFromMemory(in ubyte[] data, HipAudioEncoding encoding, HipAudioType type, 133 void delegate(in ubyte[]) onSuccess, void delegate() onFailure) 134 { 135 this.type = type; 136 this.isStreamed = false; 137 return decoder.loadData(data, encoding, type, hint, onSuccess, onFailure); 138 } 139 /** 140 * Decodes a bit more of the current buffer 141 */ 142 public final uint updateStream() 143 { 144 ErrorHandler.assertExit(chunkSize > 0, "Can't update stream with 0 sized buffer."); 145 uint dec = decoder.updateDecoding(outBuffer); 146 totalDecoded+= dec; 147 onUpdateStream(outBuffer, dec); 148 return dec; 149 } 150 package final HipAudioBufferWrapper* findBuffer(HipAudioBuffer buf) 151 { 152 foreach(ref b; buffersCreated) 153 if(b.buffer == buf) 154 return &b; 155 return null; 156 } 157 158 /** 159 * Attempts to get a buffer from the buffer recycler. 160 * Used for when loadStreamed must set a buffer available 161 */ 162 public final HipAudioBuffer pollFreeBuffer() 163 { 164 if(buffersToRecycle.length > 0) 165 { 166 HipAudioBufferWrapper* w = &(buffersToRecycle[$ - 1]); 167 buffersToRecycle.length--; 168 w.isAvailable = false; 169 return w.buffer; 170 } 171 return HipAudioBuffer.init; 172 } 173 174 public final HipAudioBuffer getBuffer(ubyte[] data, uint size) 175 { 176 HipAudioBuffer ret; 177 if((ret = pollFreeBuffer()) != HipAudioBuffer.init) 178 { 179 setBufferData(&ret, data, size); 180 return ret; 181 } 182 HipAudioBufferWrapper w = createBuffer(data); 183 setBufferData(&w.buffer, data, size); 184 ret = w.buffer; 185 buffersCreated~=w; 186 return ret; 187 } 188 HipAudioBufferAPI* _getBufferAPI(ubyte[] data, uint size) 189 { 190 HipAudioBuffer* temp = new HipAudioBuffer(); 191 *temp = getBuffer(data, size); 192 return cast(HipAudioBufferAPI*)temp; 193 } 194 IHipAudioClip getAudioClipBackend(){return this;} 195 196 package final void setBufferAvailable(HipAudioBuffer buffer) 197 { 198 HipAudioBufferWrapper* w = findBuffer(buffer); 199 ErrorHandler.assertExit(w != null, "AudioClip Error: No buffer was found when trying to set it available"); 200 buffersToRecycle~= *w; 201 w.isAvailable = true; 202 } 203 204 /** 205 * Saves which data should be decoded and do 1 decoding frame 206 */ 207 public uint loadStreamed(in ubyte[] data, HipAudioEncoding encoding) 208 { 209 dataToDecode = cast(ubyte[])data; 210 this.encoding = encoding; 211 ErrorHandler.assertExit(chunkSize > 0, "Can't update stream with 0 sized buffer."); 212 uint dec = decoder.startDecoding(dataToDecode, outBuffer, chunkSize, encoding); 213 totalDecoded+= dec; 214 onUpdateStream(outBuffer, dec); 215 return dec; 216 } 217 218 ///Returns the streambuffer if streamed, else, returns entire sound 219 public ubyte[] getClipData() 220 { 221 if(isStreamed) 222 return outBuffer; 223 return decoder.getClipData(); 224 } 225 ///Returns how much has been decoded 226 public size_t getClipSize() 227 { 228 if(isStreamed) 229 return totalDecoded; 230 return decoder.getClipSize(); 231 } 232 public float getDuration(){return decoder.getDuration();} 233 public final uint getSampleRate(){return decoder.getSamplerate();} 234 public final float getDecodedDuration() 235 { 236 AudioConfig cfg = decoder.getAudioConfig(); 237 import hip.console.log; 238 rawlog(cfg.getBitDepth, cfg.channels, cfg.sampleRate); 239 return getClipSize() / (cast(float) cfg.sampleRate); 240 } 241 242 243 public void unload() 244 { 245 decoder.dispose(); 246 foreach (ref b; buffersCreated) 247 destroyBuffer(&b.buffer); 248 buffersCreated.length = 0; 249 if(outBuffer != null) 250 { 251 destroy(outBuffer); 252 outBuffer = null; 253 } 254 } 255 }